Skip to content

Conversation

asolntsev
Copy link
Contributor

@asolntsev asolntsev commented Sep 20, 2025

User description

When CDP url is not accessible, the command new Augmenter().augment(remoteWebDriver) fails immediately - even if I don't want to use CDP or BiDi.

🔗 Related Issues

This PR fixes the problem described in #9803, #10132, selenide/selenide#2797, selenide/selenide#3107 etc.

💥 What does this PR do?

This PR fixes DevToolsProvider and BiDiProvider, so that augmentation of HasBiDi and HasDevTools interfaces is now lazy-loaded.

Context:

  1. We run a remote Chrome driver in TestContainer / Selenoid / Moon / Selenium Grid
  2. The CDP url is not accessible (because it contains the port which is accessible only inside the docker)
  3. BUT I don't want to use CDP or BiDi.
  4. I use Augmenter to cast my webdriver, say, to JavascriptExecutor:
WebDriver wd = new Augmenter().augment(remoteWebDriver);  // FAILS
JavascriptExecutor js = (JavascriptExecutor) wd;

This code fails because of CDP connectivity issue during augmentation:

org.openqa.selenium.remote.http.ConnectionFailedException: JdkWebSocket initial request execution error
Build info: version: '4.35.0', revision: '1c58e5028b'
System info: os.name: 'Mac OS X', os.arch: 'aarch64', os.version: '26.0', java.version: '17.0.14'
Driver info: driver.version: unknown
	at org.openqa.selenium.remote.http.jdk.JdkHttpClient.openSocket(JdkHttpClient.java:253)
	at org.openqa.selenium.devtools.Connection.<init>(Connection.java:89)
	at org.openqa.selenium.devtools.SeleniumCdpConnection.<init>(SeleniumCdpConnection.java:36)
	at org.openqa.selenium.devtools.SeleniumCdpConnection.lambda$create$2(SeleniumCdpConnection.java:103)
	at java.base/java.util.Optional.map(Optional.java:260)
	at org.openqa.selenium.devtools.SeleniumCdpConnection.create(SeleniumCdpConnection.java:103)
	at org.openqa.selenium.devtools.SeleniumCdpConnection.create(SeleniumCdpConnection.java:49)
	at org.openqa.selenium.devtools.DevToolsProvider.getImplementation(DevToolsProvider.java:50)
	at org.openqa.selenium.devtools.DevToolsProvider.getImplementation(DevToolsProvider.java:29)
	at org.openqa.selenium.remote.Augmenter.augment(Augmenter.java:202)
	at org.openqa.selenium.remote.Augmenter.augment(Augmenter.java:173)

🔄 Types of changes

  • Bug fix (backwards compatible)

PR Type

Bug fix


Description

  • Make HasBiDi and HasDevTools augmentation lazy-loaded

  • Fix connection failures when CDP/BiDi URLs are inaccessible

  • Enable augmentation to succeed even without CDP/BiDi access

  • Implement double-checked locking pattern for thread safety


Diagram Walkthrough

flowchart LR
  A["Augmenter.augment()"] --> B["DevToolsProvider/BiDiProvider"]
  B --> C["Lazy Implementation"]
  C --> D["Connection Created Only When Accessed"]
  D --> E["No Immediate Connection Failure"]
Loading

File Walkthrough

Relevant files
Bug fix
BiDiProvider.java
Implement lazy BiDi connection initialization                       

java/src/org/openqa/selenium/bidi/BiDiProvider.java

  • Replace immediate BiDi connection creation with lazy-loaded
    implementation
  • Add double-checked locking pattern for thread-safe initialization
  • Move connection logic inside maybeGetBiDi() method
  • Make getBiDiUrl() method static
+20/-7   
DevToolsProvider.java
Implement lazy DevTools connection initialization               

java/src/org/openqa/selenium/devtools/DevToolsProvider.java

  • Replace immediate DevTools connection creation with lazy-loaded
    implementation
  • Add double-checked locking pattern for thread-safe initialization
  • Move CDP connection logic inside maybeGetDevTools() method
  • Defer version detection and connection creation until first access
+18/-6   

@asolntsev asolntsev marked this pull request as draft September 20, 2025 16:23
@selenium-ci selenium-ci added C-java Java Bindings B-devtools Includes everything BiDi or Chrome DevTools related labels Sep 20, 2025
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis ❌

9803 - Partially compliant

Compliant requirements:

  • Avoid premature failures when DevTools is not intended to be used.
  • Ensure augmentation works so DevTools can be obtained when supported.

Non-compliant requirements:

  • Allow using DevTools with RemoteWebDriver in Grid without ClassCastException when casting to HasDevTools.

Requires further human verification:

  • Validate at runtime that casting to HasDevTools on an augmented RemoteWebDriver succeeds in a Selenium Grid and DevTools session can be created when CDP is reachable.
  • Confirm no connection attempt happens during augmentation, only upon first use, by running in a Docker/grid with inaccessible CDP endpoint.

10132 - Partially compliant

Compliant requirements:

  • Do not require CDP connectivity at augmentation time.

Non-compliant requirements:

  • Fix ClassCastException when casting augmented RemoteWebDriver to HasDevTools with Augmenter.
  • Ensure DevTools acquisition works with selenium/standalone-chrome.

Requires further human verification:

  • Run the provided snippet against selenium/standalone-chrome and verify casting and devTools.createSession() succeed when CDP is available, and that augmentation itself does not fail when CDP is not available.

2797 - Not compliant

Non-compliant requirements:

  • WebDriverEventListener afterScript should include the return value of executed scripts.

3107 - Not compliant

Non-compliant requirements:

  • Handle IE alert interrupting test execution and proceed without interruption.

5678 - Not compliant

Non-compliant requirements:

  • Resolve ChromeDriver connection failures on subsequent instantiations (ConnectFailure).

1234 - Not compliant

Non-compliant requirements:

  • Ensure Selenium 2.48 triggers javascript in link href on click() in Firefox.
⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Possible Issue

The lambda captures capabilities and constructs a new WebSocket client each time HasBiDi#getBiDi() is called; verify this does not create multiple connections per driver instance or leak resources. Consider memoizing the created BiDi instance.

public HasBiDi getImplementation(Capabilities caps, ExecuteMethod executeMethod) {
  return () -> {
    URI wsUri = getBiDiUrl(caps).orElseThrow(() -> new BiDiException("BiDi not supported"));

    HttpClient.Factory clientFactory = HttpClient.Factory.createDefault();
    ClientConfig wsConfig = ClientConfig.defaultConfig().baseUri(wsUri);
    HttpClient wsClient = clientFactory.createClient(wsConfig);
    Connection connection = new Connection(wsClient, wsUri.toString());

    return Optional.of(new BiDi(connection));
  };
Behavior Change

DevTools creation is deferred; ensure that exceptions for unsupported CDP are still clear and that repeated calls do not recreate connections. Consider caching the Optional to avoid redundant connection attempts.

public HasDevTools getImplementation(Capabilities caps, ExecuteMethod executeMethod) {
  return () -> {
    Object cdpVersion = caps.getCapability("se:cdpVersion");
    String version = cdpVersion instanceof String ? (String) cdpVersion : caps.getBrowserVersion();

    CdpInfo info = new CdpVersionFinder().match(version).orElseGet(NoOpCdpInfo::new);
    Optional<DevTools> devTools =
      SeleniumCdpConnection.create(caps).map(conn -> new DevTools(info::getDomains, conn));
    return devTools;
  };

Copy link
Contributor

qodo-merge-pro bot commented Sep 20, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Prevent resource leaks with caching
Suggestion Impact:The commit implemented caching of a single BiDi instance with double-checked locking and a separate lock object, avoiding repeated client/connection creation. It also split maybeGetBiDi and getBiDi methods and stored BiDi directly instead of Optional.

code diff:

   @Override
   public HasBiDi getImplementation(Capabilities caps, ExecuteMethod executeMethod) {
     return new HasBiDi() {
-      private volatile Optional<BiDi> biDi;
+      private volatile BiDi biDi;
+      private final Object lock = new Object();
 
       @Override
       public Optional<BiDi> maybeGetBiDi() {
+        return Optional.ofNullable(biDi);
+      }
+
+      @Override
+      public BiDi getBiDi() {
         if (biDi == null) {
-          synchronized (this) {
+          synchronized (lock) {
             if (biDi == null) {
               URI wsUri =
-                  getBiDiUrl(caps).orElseThrow(() -> new BiDiException("BiDi not supported"));
+                getBiDiUrl(caps).orElseThrow(() -> new BiDiException("BiDi not supported"));
 
               HttpClient.Factory clientFactory = HttpClient.Factory.createDefault();
               ClientConfig wsConfig = ClientConfig.defaultConfig().baseUri(wsUri);
               HttpClient wsClient = clientFactory.createClient(wsConfig);
               Connection connection = new Connection(wsClient, wsUri.toString());
 
-              biDi = Optional.of(new BiDi(connection));
+              biDi = new BiDi(connection);
             }
           }
         }

Cache the BiDi instance to prevent creating a new HTTP client and connection on
every invocation. This avoids potential resource leaks and performance issues.

java/src/org/openqa/selenium/bidi/BiDiProvider.java [47-56]

-return () -> {
-  URI wsUri = getBiDiUrl(caps).orElseThrow(() -> new BiDiException("BiDi not supported"));
-
-  HttpClient.Factory clientFactory = HttpClient.Factory.createDefault();
-  ClientConfig wsConfig = ClientConfig.defaultConfig().baseUri(wsUri);
-  HttpClient wsClient = clientFactory.createClient(wsConfig);
-  Connection connection = new Connection(wsClient, wsUri.toString());
-
-  return Optional.of(new BiDi(connection));
+return new HasBiDi() {
+  private volatile Optional<BiDi> cachedBiDi;
+  
+  @Override
+  public Optional<BiDi> getBiDi() {
+    if (cachedBiDi == null) {
+      synchronized (this) {
+        if (cachedBiDi == null) {
+          URI wsUri = getBiDiUrl(caps).orElseThrow(() -> new BiDiException("BiDi not supported"));
+          HttpClient.Factory clientFactory = HttpClient.Factory.createDefault();
+          ClientConfig wsConfig = ClientConfig.defaultConfig().baseUri(wsUri);
+          HttpClient wsClient = clientFactory.createClient(wsConfig);
+          Connection connection = new Connection(wsClient, wsUri.toString());
+          cachedBiDi = Optional.of(new BiDi(connection));
+        }
+      }
+    }
+    return cachedBiDi;
+  }
 };

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a significant issue where new HTTP clients and connections are created on each call, which can lead to resource exhaustion and performance degradation. The proposed caching solution is a robust fix for this problem.

Medium
Prevent connection recreation with caching
Suggestion Impact:The commit introduces caching of a single DevTools instance with double-checked locking and separates maybeGetDevTools and getDevTools, preventing repeated creation. This aligns with the suggestion’s intent to cache the DevTools connection.

code diff:

     return new HasDevTools() {
-      private volatile Optional<DevTools> devTools;
+      private volatile DevTools devTools;
+      private final Object lock = new Object();
 
       @Override
       public Optional<DevTools> maybeGetDevTools() {
+        return Optional.ofNullable(devTools);
+      }
+
+      @Override
+      public DevTools getDevTools() {
         if (devTools == null) {
-          synchronized (this) {
+          synchronized (lock) {
             if (devTools == null) {
               Object cdpVersion = caps.getCapability("se:cdpVersion");
               String version =
-                  cdpVersion instanceof String ? (String) cdpVersion : caps.getBrowserVersion();
+                cdpVersion instanceof String ? (String) cdpVersion : caps.getBrowserVersion();
 
               CdpInfo info = new CdpVersionFinder().match(version).orElseGet(NoOpCdpInfo::new);
               this.devTools =
-                  SeleniumCdpConnection.create(caps)
-                      .map(conn -> new DevTools(info::getDomains, conn));
+                SeleniumCdpConnection.create(caps)
+                  .map(conn -> new DevTools(info::getDomains, conn))
+                  .orElseThrow(() -> new DevToolsException("Unable to create DevTools connection"));;
             }
           }
         }

Cache the DevTools instance to avoid recreating connections and performing
version matching on every call. This prevents potential resource leaks and
unnecessary overhead.

java/src/org/openqa/selenium/devtools/DevToolsProvider.java [45-53]

-return () -> {
-  Object cdpVersion = caps.getCapability("se:cdpVersion");
-  String version = cdpVersion instanceof String ? (String) cdpVersion : caps.getBrowserVersion();
-
-  CdpInfo info = new CdpVersionFinder().match(version).orElseGet(NoOpCdpInfo::new);
-  Optional<DevTools> devTools =
-    SeleniumCdpConnection.create(caps).map(conn -> new DevTools(info::getDomains, conn));
-  return devTools;
+return new HasDevTools() {
+  private volatile Optional<DevTools> cachedDevTools;
+  
+  @Override
+  public Optional<DevTools> getDevTools() {
+    if (cachedDevTools == null) {
+      synchronized (this) {
+        if (cachedDevTools == null) {
+          Object cdpVersion = caps.getCapability("se:cdpVersion");
+          String version = cdpVersion instanceof String ? (String) cdpVersion : caps.getBrowserVersion();
+          CdpInfo info = new CdpVersionFinder().match(version).orElseGet(NoOpCdpInfo::new);
+          cachedDevTools = SeleniumCdpConnection.create(caps).map(conn -> new DevTools(info::getDomains, conn));
+        }
+      }
+    }
+    return cachedDevTools;
+  }
 };

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly points out that repeatedly creating DevTools connections and performing version matching is inefficient and can lead to resource leaks. The proposed caching mechanism is an effective solution to ensure the connection is created only once.

Medium
Learned
best practice
Ensure resources are properly closed

Ensure created clients/connections are closed when no longer needed by using
try-with-resources or by wiring them to a closeable BiDi that disposes
underlying resources.

java/src/org/openqa/selenium/bidi/BiDiProvider.java [47-56]

 return () -> {
   URI wsUri = getBiDiUrl(caps).orElseThrow(() -> new BiDiException("BiDi not supported"));
 
   HttpClient.Factory clientFactory = HttpClient.Factory.createDefault();
   ClientConfig wsConfig = ClientConfig.defaultConfig().baseUri(wsUri);
   HttpClient wsClient = clientFactory.createClient(wsConfig);
   Connection connection = new Connection(wsClient, wsUri.toString());
 
-  return Optional.of(new BiDi(connection));
+  BiDi bidi = new BiDi(connection) {
+    @Override
+    public void close() {
+      super.close();
+      try {
+        wsClient.close();
+      } catch (Exception ignored) {
+      }
+    }
+  };
+  return Optional.of(bidi);
 };
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why:
Relevant best practice - Always close or dispose resources; ensure network clients and connections are shut down to prevent leaks.

Low
Dispose DevTools connections properly

Tie the lifecycle of the created DevTools to its underlying connection and
ensure the connection is closed (e.g., implement close() or attach shutdown
hook) to avoid leaks.

java/src/org/openqa/selenium/devtools/DevToolsProvider.java [45-53]

 return () -> {
   Object cdpVersion = caps.getCapability("se:cdpVersion");
   String version = cdpVersion instanceof String ? (String) cdpVersion : caps.getBrowserVersion();
 
   CdpInfo info = new CdpVersionFinder().match(version).orElseGet(NoOpCdpInfo::new);
-  Optional<DevTools> devTools =
-    SeleniumCdpConnection.create(caps).map(conn -> new DevTools(info::getDomains, conn));
-  return devTools;
+  return SeleniumCdpConnection.create(caps).map(conn -> new DevTools(info::getDomains, conn) {
+    @Override
+    public void close() {
+      super.close();
+      try {
+        conn.close();
+      } catch (Exception ignored) {
+      }
+    }
+  });
 };
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why:
Relevant best practice - Always close or dispose resources; ensure connections/listeners are shut down to prevent leaks.

Low
  • Update

@asolntsev asolntsev force-pushed the fix/devtools-augmentation branch from 7bf31b3 to 6607c45 Compare September 20, 2025 16:34
@asolntsev asolntsev marked this pull request as ready for review September 20, 2025 16:34
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

3107 - Partially compliant

Compliant requirements:

  • Allow test execution to continue when certain browser APIs/features are unavailable or unreachable.

Non-compliant requirements:

  • Avoid automation flow interruptions caused by unexpected alerts/popups in IE11 by ensuring tests can proceed without being blocked.

Requires further human verification:

  • Validate in real environments (IE11/legacy setups) that augmentation no longer fails when CDP/BiDi endpoints are unreachable and does not introduce new interruptions.

2797 - Not compliant

Non-compliant requirements:

  • AfterScript event listener should include the return value of executed scripts to improve logging.

10132 - PR Code Verified

Compliant requirements:

  • Using HasDevTools with RemoteWebDriver/Augmenter should not throw ClassCastException or fail when CDP is inaccessible.
  • Allow DevTools session creation when supported, but do not break augmentation if DevTools/CDP is unreachable.

Requires further human verification:

  • Run against Selenium Grid / dockerized Chrome to ensure augmentation no longer attempts immediate CDP connection and casting works when not using DevTools.

9803 - PR Code Verified

Compliant requirements:

  • RemoteWebDriver should be usable with HasDevTools without ClassCastException; augmentation should support lazy DevTools connection.

Requires further human verification:

  • Verify in Grid setup that HasDevTools casting succeeds and DevTools connection is deferred until first use.
⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Lazy Init Behavior

Ensure the returned lambda does not capture a failed Optional early and that exceptions from SeleniumCdpConnection.create are only raised when getDevTools() is invoked, not during augmentation.

return () -> {
  Object cdpVersion = caps.getCapability("se:cdpVersion");
  String version =
      cdpVersion instanceof String ? (String) cdpVersion : caps.getBrowserVersion();

  CdpInfo info = new CdpVersionFinder().match(version).orElseGet(NoOpCdpInfo::new);
  Optional<DevTools> devTools =
      SeleniumCdpConnection.create(caps).map(conn -> new DevTools(info::getDomains, conn));
  return devTools;
};
Error Messaging

Confirm that when BiDi is unsupported or the websocket URL is missing/unreachable, the lazy supplier throws a clear BiDiException only upon invocation, and that augmenting other interfaces remains unaffected.

return () -> {
  URI wsUri = getBiDiUrl(caps).orElseThrow(() -> new BiDiException("BiDi not supported"));

  HttpClient.Factory clientFactory = HttpClient.Factory.createDefault();
  ClientConfig wsConfig = ClientConfig.defaultConfig().baseUri(wsUri);
  HttpClient wsClient = clientFactory.createClient(wsConfig);
  Connection connection = new Connection(wsClient, wsUri.toString());

  return Optional.of(new BiDi(connection));
};

@asolntsev asolntsev self-assigned this Sep 20, 2025
Copy link
Contributor

qodo-merge-pro bot commented Sep 20, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Prevent multiple connection creation
Suggestion Impact:The commit implemented caching and lazy initialization of the BiDi instance with double-checked locking, preventing multiple connections. It changed from Optional to a single BiDi field, added maybeGetBiDi(), and synchronized on a lock object.

code diff:

     return new HasBiDi() {
-      private volatile Optional<BiDi> biDi;
+      private volatile BiDi biDi;
+      private final Object lock = new Object();
 
       @Override
       public Optional<BiDi> maybeGetBiDi() {
+        return Optional.ofNullable(biDi);
+      }
+
+      @Override
+      public BiDi getBiDi() {
         if (biDi == null) {
-          synchronized (this) {
+          synchronized (lock) {
             if (biDi == null) {
               URI wsUri =
-                  getBiDiUrl(caps).orElseThrow(() -> new BiDiException("BiDi not supported"));
+                getBiDiUrl(caps).orElseThrow(() -> new BiDiException("BiDi not supported"));
 
               HttpClient.Factory clientFactory = HttpClient.Factory.createDefault();
               ClientConfig wsConfig = ClientConfig.defaultConfig().baseUri(wsUri);
               HttpClient wsClient = clientFactory.createClient(wsConfig);
               Connection connection = new Connection(wsClient, wsUri.toString());
 
-              biDi = Optional.of(new BiDi(connection));
+              biDi = new BiDi(connection);
             }
           }
         }

To prevent resource leaks from multiple connections, cache the BiDi instance so
it is created only once. The current implementation creates a new connection
every time getBiDi() is called.

java/src/org/openqa/selenium/bidi/BiDiProvider.java [47-56]

-return () -> {
-  URI wsUri = getBiDiUrl(caps).orElseThrow(() -> new BiDiException("BiDi not supported"));
-
-  HttpClient.Factory clientFactory = HttpClient.Factory.createDefault();
-  ClientConfig wsConfig = ClientConfig.defaultConfig().baseUri(wsUri);
-  HttpClient wsClient = clientFactory.createClient(wsConfig);
-  Connection connection = new Connection(wsClient, wsUri.toString());
-
-  return Optional.of(new BiDi(connection));
+return new HasBiDi() {
+  private volatile Optional<BiDi> biDi;
+  
+  @Override
+  public Optional<BiDi> getBiDi() {
+    if (biDi == null) {
+      synchronized (this) {
+        if (biDi == null) {
+          URI wsUri = getBiDiUrl(caps).orElseThrow(() -> new BiDiException("BiDi not supported"));
+          HttpClient.Factory clientFactory = HttpClient.Factory.createDefault();
+          ClientConfig wsConfig = ClientConfig.defaultConfig().baseUri(wsUri);
+          HttpClient wsClient = clientFactory.createClient(wsConfig);
+          Connection connection = new Connection(wsClient, wsUri.toString());
+          biDi = Optional.of(new BiDi(connection));
+        }
+      }
+    }
+    return biDi;
+  }
 };

[Suggestion processed]

Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a significant issue where the PR's change to lazy-load the connection would create a new connection on every call, leading to resource leaks. The proposed fix using double-checked locking correctly implements a cached, lazy-initialized connection, which is a critical improvement for correctness and performance.

High
Learned
best practice
Ensure connection is closed

Ensure the created client/connection is properly closed when the BiDi instance
is no longer needed by wiring a close hook or using try-with-resources where
possible.

java/src/org/openqa/selenium/bidi/BiDiProvider.java [50-55]

 HttpClient.Factory clientFactory = HttpClient.Factory.createDefault();
 ClientConfig wsConfig = ClientConfig.defaultConfig().baseUri(wsUri);
 HttpClient wsClient = clientFactory.createClient(wsConfig);
 Connection connection = new Connection(wsClient, wsUri.toString());
 
-return Optional.of(new BiDi(connection));
+BiDi bidi = new BiDi(connection);
+bidi.onClose(() -> {
+  try {
+    connection.close();
+  } finally {
+    wsClient.close();
+  }
+});
+return Optional.of(bidi);
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why:
Relevant best practice - Always close or dispose resources using try-with-resources or ensure explicit shutdown to prevent leaks.

Low
  • More

@asolntsev asolntsev marked this pull request as draft September 20, 2025 16:54
@asolntsev asolntsev force-pushed the fix/devtools-augmentation branch from 6607c45 to 9e23bc1 Compare September 20, 2025 17:00
@asolntsev asolntsev marked this pull request as ready for review September 20, 2025 17:05
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

9803 - PR Code Verified

Compliant requirements:

  • RemoteWebDriver should be usable with HasDevTools without ClassCastException or premature connection attempts.
  • Augmentation should not fail when CDP endpoint is inaccessible, unless DevTools is actually used.
  • Support using RemoteWebDriver in Grid environments where CDP URL may be unreachable.

Requires further human verification:

  • Validate on real Grid/Selenoid/Moon setups that augmentation no longer fails and casting works.
  • Confirm that calling getDevTools() still connects properly when CDP is reachable.

10132 - PR Code Verified

Compliant requirements:

  • Avoid ClassCastException when casting augmented RemoteWebDriver to HasDevTools.
  • Ensure augmenter returns a proxy implementing HasDevTools even if CDP is not yet connected.
  • Defer CDP connection until DevTools is first accessed.

Requires further human verification:

  • Run an end-to-end test using selenium/standalone-chrome with remote augmentation to confirm cast succeeds and session creation works when reachable.

2797 - Not compliant

Non-compliant requirements:

  • afterScript event should include the return value of executed script.

3107 - Not compliant

Non-compliant requirements:

  • Handle IE alert interrupting automation; allow test to proceed without interruption.
⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Thread Safety

Double-checked locking uses a volatile Optional field but returns the same reference; ensure no visibility/race issues and that Optional is always non-null after initialization.

return new HasDevTools() {
  private volatile Optional<DevTools> devTools;

  @Override
  public Optional<DevTools> maybeGetDevTools() {
    if (devTools == null) {
      synchronized (this) {
        if (devTools == null) {
          Object cdpVersion = caps.getCapability("se:cdpVersion");
          String version =
            cdpVersion instanceof String ? (String) cdpVersion : caps.getBrowserVersion();

          CdpInfo info = new CdpVersionFinder().match(version).orElseGet(NoOpCdpInfo::new);
          this.devTools = SeleniumCdpConnection.create(caps).map(conn -> new DevTools(info::getDomains, conn));
        }
      }
    }
    return devTools;
  }
};
Lazy Initialization Behavior

maybeGetBiDi throws BiDiException if no URL capability is present; confirm this matches prior behavior and that augmenting alone does not trigger the exception unless accessed.

  return new HasBiDi() {
    private volatile Optional<BiDi> biDi;

    @Override
    public Optional<BiDi> maybeGetBiDi() {
      if (biDi == null) {
        synchronized (this) {
          if (biDi == null) {
            URI wsUri = getBiDiUrl(caps).orElseThrow(() -> new BiDiException("BiDi not supported"));

            HttpClient.Factory clientFactory = HttpClient.Factory.createDefault();
            ClientConfig wsConfig = ClientConfig.defaultConfig().baseUri(wsUri);
            HttpClient wsClient = clientFactory.createClient(wsConfig);
            Connection connection = new Connection(wsClient, wsUri.toString());

            biDi = Optional.of(new BiDi(connection));
          }
        }
      }
      return biDi;
    }
  };
}
Capability Handling

Version detection now deferred; verify fallback to browserVersion is correct and works when se:cdpVersion is absent, and that NoOpCdpInfo is acceptable for older/unknown versions.

    Object cdpVersion = caps.getCapability("se:cdpVersion");
    String version =
      cdpVersion instanceof String ? (String) cdpVersion : caps.getBrowserVersion();

    CdpInfo info = new CdpVersionFinder().match(version).orElseGet(NoOpCdpInfo::new);
    this.devTools = SeleniumCdpConnection.create(caps).map(conn -> new DevTools(info::getDomains, conn));
  }
}

Copy link
Contributor

qodo-merge-pro bot commented Sep 20, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Simplify lazy-loading with a memoized supplier

Replace the manual double-checked locking implementation with Guava's
Suppliers.memoize to simplify the code and improve robustness for lazy
initialization.

java/src/org/openqa/selenium/bidi/BiDiProvider.java [47-68]

 return new HasBiDi() {
-  private volatile Optional<BiDi> biDi;
+  private final Supplier<Optional<BiDi>> biDiSupplier =
+      Suppliers.memoize(
+          () -> {
+            URI wsUri =
+                getBiDiUrl(caps).orElseThrow(() -> new BiDiException("BiDi not supported"));
+
+            HttpClient.Factory clientFactory = HttpClient.Factory.createDefault();
+            ClientConfig wsConfig = ClientConfig.defaultConfig().baseUri(wsUri);
+            HttpClient wsClient = clientFactory.createClient(wsConfig);
+            Connection connection = new Connection(wsClient, wsUri.toString());
+
+            return Optional.of(new BiDi(connection));
+          });
 
   @Override
   public Optional<BiDi> maybeGetBiDi() {
-    if (biDi == null) {
-      synchronized (this) {
-        if (biDi == null) {
-          URI wsUri = getBiDiUrl(caps).orElseThrow(() -> new BiDiException("BiDi not supported"));
-
-          HttpClient.Factory clientFactory = HttpClient.Factory.createDefault();
-          ClientConfig wsConfig = ClientConfig.defaultConfig().baseUri(wsUri);
-          HttpClient wsClient = clientFactory.createClient(wsConfig);
-          Connection connection = new Connection(wsClient, wsUri.toString());
-
-          biDi = Optional.of(new BiDi(connection));
-        }
-      }
-    }
-    return biDi;
+    return biDiSupplier.get();
   }
 };
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the manual double-checked locking can be replaced by a simpler and more robust library utility (Suppliers.memoize), significantly improving code readability and maintainability.

Medium
Learned
best practice
Use initialized Optional with DCL

Initialize devTools to Optional.empty() and use a local variable in the
double-checked locking to avoid null checks and publication races.

java/src/org/openqa/selenium/devtools/DevToolsProvider.java [45-64]

 return new HasDevTools() {
-  private volatile Optional<DevTools> devTools;
+  private volatile Optional<DevTools> devTools = Optional.empty();
 
   @Override
   public Optional<DevTools> maybeGetDevTools() {
-    if (devTools == null) {
+    Optional<DevTools> local = devTools;
+    if (local.isEmpty()) {
       synchronized (this) {
-        if (devTools == null) {
+        local = devTools;
+        if (local.isEmpty()) {
           Object cdpVersion = caps.getCapability("se:cdpVersion");
           String version =
             cdpVersion instanceof String ? (String) cdpVersion : caps.getBrowserVersion();
 
           CdpInfo info = new CdpVersionFinder().match(version).orElseGet(NoOpCdpInfo::new);
-          this.devTools = SeleniumCdpConnection.create(caps).map(conn -> new DevTools(info::getDomains, conn));
+          local = SeleniumCdpConnection.create(caps).map(conn -> new DevTools(info::getDomains, conn));
+          devTools = local;
         }
       }
     }
-    return devTools;
+    return local;
   }
 };
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why:
Relevant best practice - Initialize attributes to sane defaults to avoid null/partially-initialized states, especially when using double-checked locking.

Low
  • More

@asolntsev asolntsev force-pushed the fix/devtools-augmentation branch from 9e23bc1 to 7e7bcf8 Compare September 20, 2025 17:18
@joerg1985
Copy link
Member

I have some concerns about this PR:

  • At least for BDI the URI should only be inside the capabilities when requested by the user, so for BiDi this should not needed.
    In case a feature the client requested is not working, the augmentation should fail in my mind.
  • I guess most people have a retry on startup error implemented, so the retry coukd have handled this in the past and now the exception happens later and might not handled by this any more.

@asolntsev
Copy link
Contributor Author

asolntsev commented Sep 21, 2025

  • At least for BDI the URI should only be inside the capabilities when requested by the user, so for BiDi this should not needed.

@joerg1985
Maybe for BiDi, yes. But for HasDevTools, it's automatically enabled for all Chromium browsers (at least). Users cannot avoid it (even if they don't need to use DevTools). That's the root problem.

In case a feature the client requested is not working, the augmentation should fail in my mind.

No, it should not. The goal of augmentation is NOT to check all the connectivity issues. The goal is to CAST RemoteWebDriver to the needed interface. Only to CAST, not to call its methods.

  • I guess most people have a retry on startup error implemented, so the retry coukd have handled this in the past and now the exception happens later and might not handled by this any more.

But the augmentation is NOT automatically called on startup.
The augmentation happens only user explicitly calls new Augmenter().augment(remoteWebDriver). So there is no automated retry out-of-the-box anyway.

AND if I really want to check BiDi connectivity, I would explicitly call some BiDi method on startup.

One argument for this PR:

  1. All other AugmenterProviders ARE lazy. See AddHasPermissions, AddHasCdp, AddHasExtensions, AddHasLogEvents, AddHasContext, AddHasNetworkConditions - they all have getImplementation of form:
  public HasDebugger getImplementation(Capabilities capabilities, ExecuteMethod executeMethod) {
    return () -> { ... ALL THE WORK ONLY HERE ...}
  }

  // or

  public HasPermissions getImplementation(Capabilities capabilities, ExecuteMethod executeMethod) {
    return new HasPermissions() {
          ... ALL THE WORK ONLY HERE ...
    };
  }
  1. Those people need to check connectivity on startup, they have an option: call any BiDi method.
    But those people who don't need BIdi or DevTools (like in my case), just cannot avoid it. They have no way to avoid this connectivity issue.

@joerg1985
Copy link
Member

@asolntsev i only wanted to point to this change in behaviour, i think having the same behaviour for all augmentations is a good idea. So i only have one comment left, all other areas rely on external synchronizations in case multiple threads are involved.
Is there a specific need to sychronize here or could this be removed?

@asolntsev
Copy link
Contributor Author

Is there a specific need to sychronize here or could this be removed?

If I understand your question correctly, then yes, the synchronization is needed in this case.
This is what I mean:

@Test void augmenterThreadSafety() {
  RemoteWebDriver driver = new RemoteWebDriver(gridUrl(), new ChromeOptions());
  WebDriver webDriver = new Augmenter().augment(driver);
  HasDevTools devTools = (HasDevTools) webDriver;

    for (int i = 0; i < 10; i++) {
      new Thread(() -> {
        devTools.getDevTools().getDomains(); // would create multiple instances of `DevTools` without synchronization
      }).start();
   }
}

@joerg1985
Copy link
Member

@asolntsev in my mind the client code has to take care about synchronization, in your example:

  RemoteWebDriver driver = new RemoteWebDriver(gridUrl(), new ChromeOptions());
  WebDriver webDriver = new Augmenter().augment(driver);
  HasDevTools devTools = (HasDevTools) webDriver;

    for (int i = 0; i < 10; i++) {
      new Thread(() -> {
	synchronized (devTools) {
        	devTools.getDevTools().getDomains(); 
	}
      }).start();
   }
}

Other areas of the selenium code do not expect the client code will use different threads.
So why expecting it inside the HasBiDi / HasDevTools implementation?

e.g. the code below will end in chaos without synchronization in the client code:

  RemoteWebDriver driver = new RemoteWebDriver(gridUrl(), new ChromeOptions());

    for (int i = 0; i < 10; i++) {
      new Thread(() -> {
	driver.findElement(By.id("x")).click();
      }).start();
   }
}

@asolntsev
Copy link
Contributor Author

in my mind the client code has to take care about synchronization

  1. You cannot expect from all Selenium users that they can do synchronization properly.
  2. I sincerely don't understand why should we intentionally make Selenium less secure. This synchronization is very easy to do, it's cheap, it's safe. I see zero problems with it.

the code below will end in chaos without synchronization in the client code:

Yes, maybe this code wouldn't do anything reasonable, but it's safe. I mean, it doesn't cause connection leak etc.
While the code above will cause connection leak.

@joerg1985
Copy link
Member

@asolntsev lets see what others think about this

@nvborisenko
Copy link
Member

Easy, should be thread-safe.

@asolntsev
Copy link
Contributor Author

Easy, should be thread-safe.

@nvborisenko Great! Then can we already merge this PR?

@nvborisenko
Copy link
Member

Sorry, I am not from Java world.

How I see the situation how it should be:

var driver = new AnyDriver(); // it is still NOT initializing DevTools/BiDi connection

var devtools = driver.getDevTools(); // only here new DevTools object is allocated, which initializes underlying network connection

var devtools2 = driver.getDevTools(); // it should return previously initialized object

var devtoolsN = Parallel.10Times(() => driver.getDevTools()); // it should be thread-safe and initialize one single object

If this is what you propose, then I approve PR verbally :)

@asolntsev
Copy link
Contributor Author

If this is what you propose, then I approve PR verbally :)

Yes, this is exactly my proposal. :)

Copy link
Member

@nvborisenko nvborisenko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this approach also avoids resource leakage. Sorry, I approved approach, but not code.. let's wait experts.

@titusfortner
Copy link
Member

I don't have time to dig into the details here, but the current mechanism for avoiding problems with inaccessible devtools is to use "se:cdpEnabled": false in capabilities so HasDevTools won't be added. HasBiDi won't be added unless websocketUrl is true, which should be false by default.

I'd say more magic is bad, but Augmenter is already a lot of magic, so... :)

@titusfortner titusfortner added the A-needs decision TLC needs to discuss and agree label Sep 28, 2025
@asolntsev
Copy link
Contributor Author

avoid inaccessible devtools by "se:cdpEnabled": false in capability.

Good point. Indeed, adding se:cdpEnabled=false would fix the initial problem. :)

But still, I believe this PR is not about magic at all. It just makes two tiny object lazy-loaded. Absolutely reasonable.
It doesn't even deserve all this long discussion above. :(

P.S. Ironically, capability named "cdp" ("se:cdpEnabled") does NOT affect CDP.


@Override
public HasBiDi getImplementation(Capabilities caps, ExecuteMethod executeMethod) {
return new HasBiDi() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not 100% sure about the BiDi part because the user is expressing the intention to use BiDi by setting webSocketUrl: true. I think it makes sense for CDP because we are doing that implicitly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's right: users can enable or disable BiDi.
But it doesn't matter. Anyway, it's not needed to establish BiDi connection while no BiDi methods are called.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get that, but if the user has set the capability, then it means they want to use BiDi methods. I would prefer to avoid this change. The CDP one seems fine.

Copy link
Contributor Author

@asolntsev asolntsev Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, why? If users want to use BiDi methods, they will use BiDi methods.
I still don't see any reasons to perform side effects during augmentation.

  • Augmenter should only augment (create instance of interfaces).
  • Augmenter should not perform any other actions (establish CDP connection, establish BiDi connection etc.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the user has set the capability, then it means they want to use BiDi methods

This is big mistake. If I set some capability, it means I set capability. I don't know who is BiDi.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nvborisenko Diego meant a very specific capability "se:cdpEnabled" which allows to disable the BiDi connection.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not 100% sure about the BiDi part because the user is expressing the intention to use BiDi by setting webSocketUrl: true. I think it makes sense for CDP because we are doing that implicitly.

@diemol What can I do to get this PR merged?
At least you agree that it makes sense for CDP, right?
Do you want me to revert the BiDiProvider part and keep only DevToolsProvider change? If I do so, will you merge the PR?

(just be worried that then BiDiProvider and DevToolsProvider will behave differently: one would be lazy-loaded and the other eager. One has side effect, and all the other providers don't.)

Otherwise, command `new Augmenter().augment(remoteWebDriver)` fails immediately (even if I don't want to use CDP or BiDi).

* Augmenter should only augment (create instance of interfaces).
* Augmenter should not perform any other actions (establish CDP connection, establish BiDi connection etc.)
@asolntsev asolntsev force-pushed the fix/devtools-augmentation branch from ff739c7 to f36cdfa Compare October 3, 2025 20:32
1. method `getBiDi`/`getDevTools` should establish a connection, BUT
1. method `maybeGet*()` returns connection only if the connection is already established, but should NOT establish a new connection.

It's because method `maybeGet*()` is used from `WebDriver.close()` and `WebDriver.quite()`. At this moment, we don't want a new connection, we only want to close the existing connection.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-needs decision TLC needs to discuss and agree B-devtools Includes everything BiDi or Chrome DevTools related C-java Java Bindings Review effort 2/5
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants